<?php

namespace StudyBuddy;

/**
 * Class for generating and
 * maintaining records of per-collection
 * auto-increment ids
 *
 */
class MongoIncrementor {

    /**
     * Object of type StudyBuddy\Mongo
     * Enter description here ...
     * @var object of type \StudyBuddy\Mongo
     */
    protected $oMongo;

    public function __construct(Mongo $oMongo) {
        $this->oMongo = $oMongo;
    }

    /**
     * The pseudo auto increment handling is done
     * by storing collectionName => id
     * in Autoincrements collection
     *
     * We get value, increment it and resave it
     * but watch for Errors/Exceptions in order
     * to prevent race condition
     *
     * @param string $collName which collection this id
     * is for. This has nothing to do with the name of the collection
     * where these generated sequence numbers are stored.
     * For example if you need the next id for collection 'STUDENTS',
     * then you pass the 'STUDENTS' as $collName value
     * This way different values of 'next id' are maintained
     * per collection name
     *
     * @param int $initialId if there is no record
     * for the collection yet, then start the increment counter
     * with this value.
     *
     * @param int $try this is used for recursive calling this method
     * You should NEVER pass this value yourself
     *
     * @return int value of next id for the collection
     */
    public function nextValue($collName, $initialId = 0, $try = 1) {

        if ($try > 100) {
            throw new \RuntimeException('Unable to get nextID for collection ' . $collName . ' after 100 tries');
        }

        $prevRecordID = null;
        $coll = $this->oMongo->Autoincrements;
        $coll->ensureIndex(array('coll' => 1, 'id' => 1), array('unique' => true));

        /**
         * We use find() instead of findOne() for a reason!
         * It's just more reliable this way
         */
        $cursor = $coll->find(array('coll' => $collName))->sort(array('id' => -1))->limit(1);
        if ($cursor && $cursor->hasNext()) {
            $a = $cursor->getNext();
            $prevRecordID = $a['_id'];
        } else {
            $a = array('coll' => $collName, 'id' => $initialId);
        }

        $prevID = $a['id'];
        $newId = ($a['id'] + 1);

        /**
         * Remove the _id from record, otherwise
         * we will be unable to insert
         * a new record if it already has the same _id
         * This way a new _id will be auto-generated for us
         */
        unset($a['_id']);
        $a['id'] = $newId;


        /**
         * Wrapping this inside try/catch so that if
         * another process inserts the same value of coll/id
         * between the time we selected and updated this
         * it will throw exception or return false and then
         * we will try again up to 100 times
         *
         * In Case of duplicate key Mongo throws Exception,
         * but just in case it will change in the future,
         * we also test if $ret is false
         */
        try {
            /**
             * Using fsync=>true because its very critically important
             * to actually write the row to disc, otherwise if database
             * goes down we will lose the correct value
             * of our increment ID
             */
            $ret = $coll->insert($a, array('fsync' => true));
            if (!$ret) {
                $try++;

                return $this->nextValue($collName, $initialId, $try);
            }

            /**
             * Insert successfull
             * now delete previous record(s)
             */
            if (null !== $prevRecordID) {
                $removed = $coll->remove(array('_id' => $prevRecordID)); //, array('fsync' => true) // not very important to fsync
            }
        } catch (\MongoException $e) {

            $try++;

            return $this->nextValue($collName, $initialId, $try);
        }

        return $newId;
    }

}
